package com.stark.commons.lang.util;

import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 压缩 zip 包方法集合。
 * @author Ben
 * @since 2022/3/24
 * @version 1.0.0
 */
public class ZipUtil {

	/**
	 * 压缩后写入 zip 文件。
	 * @param files 压缩文件列表。
	 * @param zipPath zip 文件绝对路径。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	public static void zipToFile(List<ZipFile> files, String zipPath) throws IOException {
		FileOutputStream out = new FileOutputStream(zipPath);
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(out);
			for (ZipFile file : files) {
				zip(zos, file, "");
			}
		} finally {
			if (zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 压缩后写入输出流。
	 * @param files 压缩文件列表。
	 * @param out 输出流，自动关闭。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	public static void zipToOutputStream(List<ZipFile> files, OutputStream out) throws IOException {
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(out);
			for (ZipFile file : files) {
				zip(zos, file, "");
			}
		} finally {
			if (zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 压缩后返回 zip 文件字节码。
	 * <p>若压缩后的 zip 文件大小超过【<i>1.0 * Integer.MAX_VALUE / 1024 / 1024 兆（64 位系统下 2G）</i>】时，压缩失败抛出异常。
	 * @param files 压缩文件列表。
	 * @return zip 文件字节码。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	public static byte[] zipToBytes(List<ZipFile> files) throws IOException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(out);
			for (ZipFile file : files) {
				zip(zos, file, "");
			}
		} finally {
			if (zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return out.toByteArray();
	}

	/**
	 * 压缩本地文件后写入 zip 文件。
	 * @param files 压缩文件列表。
	 * @param zipPath zip 文件绝对路径。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	public static void zipToFileLocal(List<File> files, String zipPath) throws IOException {
		FileOutputStream out = new FileOutputStream(zipPath);
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(out);
			for (File file : files) {
				zipLocal(zos, file, "");
			}
		} finally {
			if (zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 压缩本地文件后写入输出流。
	 * @param files 压缩文件列表。
	 * @param out 输出流，自动关闭。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	public static void zipToOutputStreamLocal(List<File> files, OutputStream out) throws IOException {
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(out);
			for (File file : files) {
				zipLocal(zos, file, "");
			}
		} finally {
			if (zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 压缩本地文件后返回 zip 文件字节码。
	 * <p>若压缩后的 zip 文件大小超过【<i>1.0 * Integer.MAX_VALUE / 1024 / 1024 兆（64 位系统下 2G）</i>】时，压缩失败抛出异常。
	 * @param files 压缩文件列表。
	 * @return zip 文件字节码。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	public static byte[] zipToBytesLocal(List<File> files) throws IOException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(out);
			for (File file : files) {
				zipLocal(zos, file, "");
			}
		} finally {
			if (zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return out.toByteArray();
	}

	/**
	 * 解压 zip 压缩包。
	 * @param zip zip 包。
	 * @param destPath 解压路径。
	 * @throws IOException 解压文件失败时抛出异常。
	 */
	public static void unzip(byte[] zip, String destPath) throws IOException {
		ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zip));
		unzip(zis, destPath);
	}

	/**
	 * 解压 zip 压缩包。
	 * @param zipPath zip 包路径。
	 * @param destPath 解压路径。
	 * @throws IOException 解压文件失败时抛出异常。
	 */
	public static void unzip(String zipPath, String destPath) throws IOException {
		ZipInputStream zis = new ZipInputStream(Files.newInputStream(Paths.get(zipPath)));
		unzip(zis, destPath);
	}

	/**
	 * 压缩文件对象。
	 * @author Ben
	 * @since 2022/3/24
	 * @version 1.0.0
	 */
	@Data
	@Accessors(chain = true)
	public static class ZipFile implements Serializable {

		private static final long serialVersionUID = 2263570583581052762L;

		/** 是否目录 */
		private boolean directory;

		/** 名称 */
		private String name;

		/** 目录文件 */
		private List<ZipFile> dirFiles = new ArrayList<>();

		/** 文件流 */
		private InputStream fileInputStream;

		/**
		 * 实例化目录对象。
		 * @param name 名称。
		 * @return 目录对象。
		 */
		public static ZipFile dir(String name) {
			return new ZipFile()
					.setDirectory(true)
					.setName(name);
		}

		/**
		 * 实例化目录对象。
		 * @param name 名称。
		 * @param dirFiles 目录下的文件。
		 * @return 目录对象。
		 */
		public static ZipFile dir(String name, List<ZipFile> dirFiles) {
			return new ZipFile()
					.setDirectory(true)
					.setName(name)
					.setDirFiles(dirFiles);
		}

		/**
		 * 实例化文件对象。
		 * @param name 名称。
		 * @param fileInputStream 文件流，压缩完成后自动关闭。
		 * @return 文件对象。
		 */
		public static ZipFile file(String name, InputStream fileInputStream) {
			return new ZipFile()
					.setDirectory(false)
					.setName(name)
					.setFileInputStream(fileInputStream);
		}

		/**
		 * 实例化文件对象。
		 * @param name 名称。
		 * @param fileBytes 文件字节码。
		 * @return 文件对象。
		 */
		public static ZipFile file(String name, byte[] fileBytes) {
			return new ZipFile()
					.setDirectory(false)
					.setName(name)
					.setFileInputStream(new ByteArrayInputStream(fileBytes));
		}

	}

	/**
	 * 压缩。
	 * @param zos 压缩输出流。
	 * @param file 压缩文件。
	 * @param dir 当前目录，根目录传 "" 。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	private static void zip(ZipOutputStream zos, ZipFile file, String dir) throws IOException {
		if (dir == null) {
			dir = "";
		}

		if (file.isDirectory()) {
			// 空目录
			if (CollectionUtils.isEmpty(file.getDirFiles())) {
				zos.putNextEntry(new ZipEntry(dir + (StringUtils.isBlank(dir) ? "" : File.separator) + file.getName() + File.separator));
			}
			// 非空目录
			else {
				for (ZipFile child : file.getDirFiles()) {
					zip(zos, child, dir + (StringUtils.isBlank(dir) ? "" : File.separator) + file.getName());
				}
			}
		} else {
			zos.putNextEntry(new ZipEntry(dir + (StringUtils.isBlank(dir) ? "" : File.separator) + file.getName()));

			BufferedInputStream bis = new BufferedInputStream(file.getFileInputStream());
			byte[] buffer = new byte[1024];
			int length;
			while ((length = bis.read(buffer, 0, 1024)) != -1) {
				zos.write(buffer, 0, length);
			}
			bis.close();
		}
	}

	/**
	 * 压缩本地磁盘文件。
	 * @param zos 压缩输出流。
	 * @param file 压缩文件。
	 * @param dir 当前目录，根目录传 "" 。
	 * @throws IOException 压缩文件失败时抛出异常。
	 */
	private static void zipLocal(ZipOutputStream zos, File file, String dir) throws IOException {
		if (dir == null) {
			dir = "";
		}

		if (file.isDirectory()) {
			File[] dirFiles = file.listFiles();
			// 空目录
			if (ArrayUtils.isEmpty(dirFiles)) {
				zos.putNextEntry(new ZipEntry(dir + (StringUtils.isBlank(dir) ? "" : File.separator) + file.getName() + File.separator));
			}
			// 非空目录
			else {
				for (File child : dirFiles) {
					zipLocal(zos, child, dir + (StringUtils.isBlank(dir) ? "" : File.separator) + file.getName());
				}
			}
		} else {
			zos.putNextEntry(new ZipEntry(dir + (StringUtils.isBlank(dir) ? "" : File.separator) + file.getName()));

			BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(file.toPath()));
			byte[] buffer = new byte[1024];
			int length;
			while ((length = bis.read(buffer, 0, 1024)) != -1) {
				zos.write(buffer, 0, length);
			}
			bis.close();
		}
	}

	/**
	 * 解压 zip 压缩包。
	 * @param zis zip 包输入流。
	 * @param destPath 解压路径。
	 * @throws IOException 解压文件失败时抛出异常。
	 */
	private static void unzip(ZipInputStream zis, String destPath) throws IOException {
		File destFile = new File(destPath);
		if (!destFile.exists()) {
			boolean mkdirs = destFile.mkdirs();
			if (!mkdirs) {
				throw new RuntimeException("创建目录失败");
			}
		}

		ZipEntry entry;
		String path;
		File file;
		while ((entry = zis.getNextEntry()) != null) {
			path = destPath + File.separator + entry.getName();
			file = new File(path);
			if (entry.isDirectory()) {
				boolean mkdirs = file.mkdirs();
				if (!mkdirs) {
					throw new RuntimeException("创建目录失败");
				}
			} else {
				BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(file.toPath()));
				byte[] buffer = new byte[1024];
				int length;
				while ((length = zis.read(buffer, 0, 1024)) != -1) {
					bos.write(buffer, 0, length);
				}
				bos.close();
			}
			zis.closeEntry();
		}
	}

}
